# 原生koa2实现静态资源服务器
30 秒速记
- 静态资源服务器就做一件事:把
URL路径映射到磁盘上某个目录里的文件,读出来返回 - 三步:拼本地路径 → 判断是文件、目录还是不存在 → 设
Content-Type返回内容 - 安全底线:拼完路径必须检查还在静态根目录里,不然
/../能读到服务器任意文件 - 生产要考虑流式输出、缓存头、
Range,实际一般交给Nginx、CDN或koa-static
原生实现静态资源服务器,本质就是把请求路径拼到静态目录上,去磁盘找文件,找到就按类型返回,找不到就返回 404。 这个例子的价值是让你看清 koa-static 这类中间件内部在干什么。自己写的时候最容易漏的是安全:path.join(root, ctx.url) 遇到 /../../etc/passwd 会跑出根目录,必须拼完再检查一遍。还有性能,一次性 readFile 读大文件很费内存,正经做法是用 fs.createReadStream 流式返回,再加上缓存头。上线的话我一般不会自己写,交给 Nginx 或 CDN。
理解「原生koa2实现静态资源服务器」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 network 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
面试官追问
追问 1访问不存在的 /js/missing.js,页面显示 404 文字,这样算对吗?
文字对了,但这个例子没设状态码,实际返回的是 200。浏览器和 CDN 会把它当成功的 JS 缓存起来,要写 ctx.status = 404。
追问 2path.join(staticRoot, ctx.url) 有什么安全问题?
请求 /../../etc/passwd 时(用 curl --path-as-is 就能发出去),拼出来的路径会跑到根目录外面。拼完要检查:if (!full.startsWith(staticRoot + path.sep)) return ctx.status = 403。
追问 3为什么生产上一般不用这种手写的静态服务?
缓存协商、gzip、Range 断点续传、目录穿越防护这些都要自己补,Nginx 和 CDN 早就做好了,性能也高一大截。手写版用来理解原理就够了。
追问 4静态目录写成 './static',从别的目录启动服务就找不到文件了,为什么?
path.resolve 处理相对路径时按 process.cwd() 算,也就是你执行命令的目录。用 path.join(__dirname, './static'),按入口文件所在位置算,在哪启动都一样。
# 原生koa2实现静态资源服务器
# 前言
30 秒速记
- 请求静态资源只有三种结果:是文件 → 返回内容,是目录 → 列目录或返回首页,不存在 →
404 - 判断方式:
fs.stat看isFile()还是isDirectory(),报ENOENT就是不存在 - 文件要按扩展名带对
Content-Type,不然浏览器可能不执行JS、不应用CSS - 目录列表要不要展示是安全决策,线上一般关掉,访问目录时返回
index.html或403
一个静态资源请求打过来,服务端无非三种处理:是文件就返回内容,是目录就列出里面的东西或返回首页,啥都没有就 404。 判断靠 fs.stat,返回的对象上有 isFile() 和 isDirectory(),文件不存在会报 ENOENT。这三种情况里最需要拍板的是目录:开发时列目录很方便,线上把目录结构暴露出去等于给别人送地图,一般直接关掉,访问目录时返回 index.html 或者 403。
一个http请求访问web服务静态资源,一般响应结果有三种情况
- 访问文本,例如js,css,png,jpg,gif
- 访问静态目录
- 找不到资源,抛出404错误
面试官追问
追问 1图片请求 /image/logo.png 返回的却是一段目录列表 HTML,会是什么原因?
多半是 logo.png 实际是个目录,或者路径拼错落到了上一级目录上。用 fs.statSync(fullPath).isDirectory() 打出来看一下就清楚,同时浏览器那边因为类型不对,图片显示不出来。
追问 2验收用例怎么设计,才能覆盖这三种结果?
至少四条:一个 JS 文件、一张图片、一个目录、一个不存在的路径。分别断言状态码、Content-Type 和内容,不能只看后缀猜结果。
追问 3安全评审要求不能展示目录内容,代码该怎么改?
isDirectory() 分支里先找 index.html,有就返回它,没有就 ctx.status = 403。目录列表功能整个关掉,只在本地开发时开。
追问 4单页应用希望找不到的路径都返回 index.html,图片 404 也这样处理吗?
不行。前端路由兜底只该给页面路由用,/assets/xxx.png 这种静态资源找不到必须是真 404,不然浏览器拿到一段 HTML 当图片解析,排查起来很麻烦。一般按有没有扩展名来区分。
# 原生koa2 静态资源服务器例子
30 秒速记
- 例子分三块:入口
index.js管路由和响应,util/content.js判断资源类型,dir.js、file.js各管一种 - 主流程:
ctx.url拼到静态根目录 →stat判断 → 文件读内容、目录生成链接列表 - 跑起来自己试四种请求:根路径、
index.html、一个JS、一个不存在的文件 - 这个例子用同步
API和全量读取,讲原理够用,不能直接拿去上线
这个例子用纯 Koa 加 fs、path 搭了一个能用的静态服务器,拆成几个小模块各管一件事。 入口文件接请求、算 MIME 类型、写响应;content.js 拿到本地路径后判断是文件、目录还是不存在,分别交给 file.js 和 dir.js。看懂它,再去看 koa-static 和 koa-send 的源码会轻松很多。拉下来跑的时候别只访问首页,把不存在的路径、中文文件名都试一下,能看出这种简化写法的边界在哪。
面试官追问
追问 1只看示例仓库链接,怎么现场讲清楚它做了什么?
说清楚一条请求的路径:ctx.url 拼成本地路径,existsSync 判断存在,statSync 区分文件和目录,最后按扩展名设 Content-Type 返回。讲完这条线就够了。
追问 2静态目录下放一个中文名的文件,比如 说明.txt,能访问到吗?
访问不到。浏览器会把路径编码成 %E8%AF%B4...,代码直接拿 ctx.url 去拼路径,没做 decodeURIComponent,磁盘上找不到这个名字。
追问 3请求 /index.html?v=123 带了版本号参数,为什么就 404 了?
ctx.url 包含查询字符串,拼出来的文件名是 index.html?v=123,磁盘上当然没有。应该用 ctx.path,它不带问号后面那段。
追问 4能直接把这个目录结构复制到生产项目里用吗?
不建议。同步读文件会阻塞事件循环,没防目录穿越,没有缓存头。生产直接上 koa-static,或者让 Nginx 去托管静态文件。
# 代码目录
30 秒速记
static/:要对外提供的资源,css、image、js和index.htmlutil/content.js:总调度,判断请求的是文件、目录还是不存在dir.js+walk.js:读目录、拼成带链接的HTML列表;file.js:读文件内容mimes.js:扩展名到Content-Type的映射表,新增文件类型改这里index.js:启动入口,算MIME、写响应
目录结构按职责拆得很清楚:static 放对外的资源,util 放工具函数,index.js 是入口。 content.js 是调度中心,拿到本地路径后判断是什么,再分给 file.js 或 dir.js;walk.js 负责把目录里的子目录和文件列出来,mimes.js 是一张扩展名对应类型的表。这样拆的好处是排查问题时能直接定位:类型不对看 mimes.js,目录列表不对看 walk.js,读不到文件看 file.js。
├── static # 静态资源目录
│ ├── css/
│ ├── image/
│ ├── js/
│ └── index.html
├── util # 工具代码
│ ├── content.js # 读取请求内容
│ ├── dir.js # 读取目录内容
│ ├── file.js # 读取文件内容
│ ├── mimes.js # 文件类型列表
│ └── walk.js # 遍历目录内容
└── index.js # 启动入口文件
面试官追问
追问 1全写在 index.js 里也能跑,拆这么多文件有必要吗?
这么小的项目拆不拆差别不大,但拆开后每个模块能单独测、单独替换,比如把 file.js
